import cv2
import numpy as np
from PIL import ImageFile, Image, ImageDraw, ImageFont


class ImagesStitcher:
    """
    图像拼接
    """

    def stitch(self, images, ratio=0.75, reproj_thresh=4.0, show_matches=False):
        # 拼接函数
        # 获取输入图片
        (image_b, image_a) = images
        # 检测A、B图片的SIFT关键特征点，并计算特征描述子
        (kps_a, features_a) = self.get_features_site(image_a)
        (kps_b, features_b) = self.get_features_site(image_b)

        # 匹配两张图片的所有特征点，返回匹配结果
        matches_info = self.match_key_points(kps_a, kps_b, features_a, features_b, ratio, reproj_thresh)

        # 如果返回结果为空，没有匹配成功的特征点，退出算法
        if matches_info is None:
            return None

        # 否则，提取匹配结果
        # H是3x3视角变换矩阵
        (matches, h, status) = matches_info
        # 将图片A进行视角变换，result是变换后图片
        result = cv2.warpPerspective(image_a, h, (image_a.shape[1] + image_b.shape[1], image_a.shape[0]))

        # 将图片B传入result图片最右端
        result[:image_a.shape[0], :image_a.shape[1]] = image_b

        # 检测是否需要显示图片匹配
        if show_matches:
            # 生成匹配图片
            vis = self.draw_matches(image_a, image_b, kps_a, kps_b, matches, status)
            # 返回结果
            return (result, vis)

        # 返回匹配结果
        return result

    @staticmethod
    def get_features_site(image):
        """
        获取特征值
        """
        # 将彩色图片转换成灰度图
        # gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

        # 建立SIFT生成器
        descriptor = cv2.SIFT_create()
        # 检测SIFT特征点，并计算描述子
        (kps, features) = descriptor.detectAndCompute(image, None)

        # 将结果转换成NumPy数组
        kps = np.float32([kp.pt for kp in kps])

        # 返回特征点集，及对应的描述特征
        return (kps, features)

    @staticmethod
    def match_key_points(kps_a, kps_b, features_a, features_b, ratio, reproj_thresh):
        """
        匹配关键点
        """
        # 建立暴力匹配器
        matcher = cv2.BFMatcher()

        # 使用KNN检测来自 a、b 图的SIFT特征匹配对，K=2
        raw_matches = matcher.knnMatch(features_a, features_b, 2)

        matches = []
        for matches_info in raw_matches:
            # 当最近距离跟次近距离的比值小于ratio值时，保留此匹配对
            if len(matches_info) == 2 and matches_info[0].distance < matches_info[1].distance * ratio:
                # 存储两个点在features_a, featuresB中的索引值
                matches.append((matches_info[0].trainIdx, matches_info[0].queryIdx))

        # 当筛选后的匹配对大于4时，计算视角变换矩阵
        if len(matches) > 4:
            # 获取匹配对的点坐标
            pts_a = np.float32([kps_a[i] for (_, i) in matches])
            pts_b = np.float32([kps_b[i] for (i, _) in matches])

            # 计算视角变换矩阵
            (h, status) = cv2.findHomography(pts_a, pts_b, cv2.RANSAC, reproj_thresh)

            # 返回结果
            return (matches, h, status)

        # 如果匹配对小于4时，返回None
        return None

    @staticmethod
    def draw_matches(image_a, image_b, kps_a, kps_b, matches, status):
        # 初始化可视化图片，将A、B图左右连接到一起
        (h_a, w_a) = image_a.shape[:2]
        (h_b, w_b) = image_b.shape[:2]
        vis = np.zeros((max(h_a, h_b), w_a + w_b, 3), dtype="uint8")
        vis[0:h_a, 0:w_a] = image_a
        vis[0:h_b, w_b:] = image_b

        # 联合遍历，画出匹配对
        for ((trainIdx, queryIdx), s) in zip(matches, status):
            # 当点对匹配成功时，画到可视化图上
            if s == 1:
                # 画出匹配对
                pt_a = (int(kps_a[queryIdx][0]), int(kps_a[queryIdx][1]))
                pt_b = (int(kps_b[trainIdx][0]) + w_a, int(kps_b[trainIdx][1]))
                cv2.line(vis, pt_a, pt_b, (0, 255, 0), 1)
        # 返回可视化结果
        return vis


class OpencvManager(object):
    """
    opencv图片管理器
    opencv-python  4.11.0.86
    """

    def __init__(self):
        self.image_info = None

    @staticmethod
    def write_image(save_path, image):
        """
        图片保存
        :param save_path: 保存路径
        :param image: 图片信息
        :return:"""
        cv2.imwrite(save_path, image)

    def image_show(self, show_name, image_info=None, need_save=False, save_path=None):
        """
        图片显示
        :param show_name: 显示名称
        :param image_info: 图片信息
        :param need_save: 是否保存
        :param save_path: 保存路径
        :return:
        """
        if image_info is None:
            image_info = self.image_info
        print(f"图片形状：image.shape  ：{image_info.shape}")
        print(f"图片大小：image.size  ：{image_info.size}")
        print(f"图片数据位数：image.dtype  ：{image_info.dtype}")
        print(f"图片数据类型：type(img)  ：{type(image_info)}")
        print(f"图片数据：image  ：{image_info}")
        # 图像的显示,也可以创建多个窗口
        if need_save and save_path is not None:
            self.write_image(save_path, image_info)
        cv2.imshow(show_name, image_info)
        # 等待时间，毫秒级，0表示任意键终止
        cv2.waitKey(0)
        cv2.destroyAllWindows()

    @staticmethod
    def video_show(show_name, video):
        """
        视频播放
        :param show_name: 显示名称
        :param video: 视频信息
        :return:
        """
        vc = cv2.VideoCapture(video)
        while open:
            ret, frame = vc.read()
            if frame is None:
                break
            if ret == True:
                gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
                cv2.imshow(show_name, gray)
                if cv2.waitKey(100) & 0xFF == 27:
                    break
        vc.release()
        cv2.destroyAllWindows()

    def read_image(self, image_path, single_channel=False):
        """
        图片读取
        :param image_path: 图片路径
        :param single_channel: 黑白图读取
        :return:
        """
        # opencv读取的格式是BGR
        if single_channel:
            # 读取为灰度值
            self.image_info = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
        else:
            self.image_info = cv2.imread(image_path)

        self.image_show(f"read_image_single_channel:{single_channel}", self.image_info)
        return self.image_info

    def read_video(self, video_path):
        """
        视频读取
        :param video_path: 视频路径
        :return:
        """
        vc = cv2.VideoCapture(video_path)
        # 检查是否打开正确
        if vc.isOpened():
            oepn, frame = vc.read()
        else:
            open = False
        # vc.release()
        # cv2.destroyAllWindows()
        self.video_show("read_video", video_path)

    def color_channel_extraction(self, image_path):
        """
        颜色通道提取
        :param image_path: 图片路径
        :return:
        """
        img = cv2.imread(image_path)
        b, g, r = cv2.split(img)
        img = cv2.merge((b, g, r))
        self.image_show("color_channel_extraction_1", img)

        # 只保留R
        self.image_info = img.copy()
        self.image_info[:, :, 0] = 0
        self.image_info[:, :, 1] = 0
        self.image_show('R', self.image_info)

        # 只保留G
        self.image_info = img.copy()
        self.image_info[:, :, 0] = 0
        self.image_info[:, :, 2] = 0
        self.image_show('G', self.image_info)

        # 只保留B
        self.image_info = img.copy()
        self.image_info[:, :, 1] = 0
        self.image_info[:, :, 2] = 0
        self.image_show('B', self.image_info)

        # 通道交换
        self.image_info = img.copy()
        self.image_info = self.image_info[:, :, ::-1]
        self.image_show('C', self.image_info)

        # 图片转换
        self.image_info = img.copy()
        self.image_info = cv2.cvtColor(self.image_info, code=cv2.COLOR_BGR2GRAY)  # 转换为黑白
        self.image_show('D', self.image_info)

    def change_image_arrange(self, image_path, arrangement=None):
        """
        改变图片排列
        :param image_path: 图片路径
        :param arrangement: 排列方式  上下交换   左右交换
        :return:
        """
        img = self.read_image(image_path)
        if arrangement is None:
            self.image_info = img[::-1, :, :]
            self.image_show("UP")
        else:
            self.image_info = img[:, ::-1, :]
            self.image_show("right")

    def change_image_size(self, image_path, length, width, capture=False):
        """
        改变图片大小
        :param image_path: 图片路径
        :param length: 长度
        :param width: 宽度
        :param capture: 是否截取  截取 或者缩放
        :return:
        """
        image_info = cv2.imread(image_path)
        if capture:
            cat = image_info[length[0]:length[1], width[0]:width[1]]
            self.image_show("image_capture", cat)
        else:
            self.image_info = cv2.resize(image_info, (length, width))
            self.image_show("image_scale", self.image_info)
            # fx X方向放大比例  fy y方向放大比例  (0, 0) 自动计算数据
            img_2 = cv2.resize(image_info, (0, 0), fx=2, fy=2)
            self.image_show("image_scale", img_2)
            img_3 = cv2.resize(image_info, (0, 0), fx=1, fy=2)
            self.image_show("image_scale", img_3)

    @staticmethod
    def boundary_filling(image_path):
        """
        边界填充
        - BORDER_REPLICATE：复制法，也就是复制最边缘像素。
        - BORDER_REFLECT：反射法，对感兴趣的图像中的像素在两边进行复制例如：fedcba | abcdefgh | hgfedcb
        - BORDER_REFLECT_101：反射法，也就是以最边缘像素为轴，对称，gfedcb | abcdefgh | gfedcba
        - BORDER_WRAP：外包装法cdefgh | abcdefgh | abcdefg
        - BORDER_CONSTANT：常量法，常数值填充。
        :param image_path: 图片路径
        :return:
        """
        img = cv2.imread(image_path)
        top_size, bottom_size, left_size, right_size = (50, 50, 50, 50)
        replicate = cv2.copyMakeBorder(img, top_size, bottom_size, left_size, right_size,
                                       borderType=cv2.BORDER_REPLICATE)
        reflect = cv2.copyMakeBorder(img, top_size, bottom_size, left_size, right_size, cv2.BORDER_REFLECT)
        reflect101 = cv2.copyMakeBorder(img, top_size, bottom_size, left_size, right_size, cv2.BORDER_REFLECT_101)
        wrap = cv2.copyMakeBorder(img, top_size, bottom_size, left_size, right_size, cv2.BORDER_WRAP)
        constant = cv2.copyMakeBorder(img, top_size, bottom_size, left_size, right_size, cv2.BORDER_CONSTANT,
                                      value=(0, 15, 65))

        import matplotlib.pyplot as plt
        plt.subplot(231), plt.imshow(img, 'gray'), plt.title('ORIGINAL')
        plt.subplot(232), plt.imshow(replicate, 'gray'), plt.title('REPLICATE')
        plt.subplot(233), plt.imshow(reflect, 'gray'), plt.title('REFLECT')
        plt.subplot(234), plt.imshow(reflect101, 'gray'), plt.title('REFLECT_101')
        plt.subplot(235), plt.imshow(wrap, 'gray'), plt.title('WRAP')
        plt.subplot(236), plt.imshow(constant, 'gray'), plt.title('CONSTANT')
        plt.show()

    def mosaic_realization(self, image_path, extent: int = 10):
        """
        马赛克生成
        :param image_path: 图片路径
        :param extent: 像素大小
        :return:
        """
        self.image_info = cv2.imread(image_path)
        image_np = self.image_info[::extent, ::extent]
        length, width, _ = self.image_info.shape
        image_np = np.repeat(image_np, extent, axis=0)
        image_np = np.repeat(image_np, extent, axis=1)
        self.image_info[:, :] = image_np[:length, :width]
        self.image_show("mosaic_realization", self.image_info)

    def save_transparent_background(self, image_path, threshold=200):
        """
        保存透明背景
        :param image_path: 图片路径
        :param threshold: 阈值
        :return:
        """
        self.image_info = cv2.imread(image_path)
        result = cv2.cvtColor(self.image_info, cv2.COLOR_BGR2BGRA)

        # 方式1 =====================
        # B, G, R = cv2.split(self.image_info)
        # _, Alpha = cv2.threshold(B, 251, 255, cv2.THRESH_BINARY)
        # # cv2.imshow('thres', Alpha)
        # B2, G2, R2, A2 = cv2.split(result)
        # A2 = Alpha
        # result = cv2.merge([B2, G2, R2, A2])  # 通道合并
        # 方式1 =====================

        # 方式2 =====================
        for i in range(self.image_info.shape[0]):  # 访问所有行
            for j in range(self.image_info.shape[1]):  # 访问所有列
                if self.image_info[i, j, 0] > threshold and self.image_info[i, j, 1] > threshold and self.image_info[
                    i, j, 2] > threshold:
                    result[i, j, 3] = 0
        # 方式2 =====================
        self.image_show("transparent_background", self.image_info)

    def image_fusion(self, image_path_1, image_path_2):
        """
        图片融合
        :param image_path_1: 图片路径1
        :param image_path_2: 图片路径2
        :return:
        """
        img_1 = cv2.imread(image_path_1)
        img_2 = cv2.imread(image_path_2)
        img_re_1 = cv2.resize(img_1, (500, 414))
        img_re_2 = cv2.resize(img_2, (500, 414))
        image_fusion_info = cv2.addWeighted(img_re_1, 0.4, img_re_2, 0.6, 0)
        self.image_show("image_fusion_info", image_fusion_info)

    def image_grayscale(self, image_path):
        """
        图片灰度值
        HSV
        - H - 色调（主波长）。
        - S - 饱和度（纯度/颜色的阴影）。
        - V值（强度）
        :param image_path: 图片路径
        :return:
        """
        img = cv2.imread(image_path)
        hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
        self.image_show("image_grayscale", hsv)

    def image_threshold(self, image_path):
        """
        图像阈值处理
        ret, dst = cv2.threshold(src, thresh, maxval, type)
        - dst： 输出图

        - src： 输入图，只能输入单通道图像，通常来说为灰度图
        - thresh： 阈值
        - maxval： 当像素值超过了阈值（或者小于阈值，根据type来决定），所赋予的值
        - type：二值化操作的类型，包含以下5种类型： cv2.THRESH_BINARY； cv2.THRESH_BINARY_INV； cv2.THRESH_TRUNC； cv2.THRESH_TOZERO；cv2.THRESH_TOZERO_INV

        - cv2.THRESH_BINARY         超过阈值部分取maxval（最大值），否则取0
        - cv2.THRESH_BINARY_INV     THRESH_BINARY的反转
        - cv2.THRESH_TRUNC          大于阈值部分设为阈值，否则不变
        - cv2.THRESH_TOZERO         大于阈值部分不改变，否则设为0
        - cv2.THRESH_TOZERO_INV     THRESH_TOZERO的反转
        :param image_path: 图片路径
        :return:
        """
        from matplotlib import pyplot as plt
        #  matplotlib    3.9.4
        img = cv2.imread(image_path)
        ret, thresh1 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
        ret, thresh2 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY_INV)
        ret, thresh3 = cv2.threshold(img, 127, 255, cv2.THRESH_TRUNC)
        ret, thresh4 = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO)
        ret, thresh5 = cv2.threshold(img, 127, 255, cv2.THRESH_TOZERO_INV)

        titles = ['Original Image', 'BINARY', 'BINARY_INV', 'TRUNC', 'TOZERO', 'TOZERO_INV']
        images = [img, thresh1, thresh2, thresh3, thresh4, thresh5]

        for i in range(6):
            plt.subplot(2, 3, i + 1), plt.imshow(images[i], 'gray')
            plt.title(titles[i])
            plt.xticks([]), plt.yticks([])
        plt.show()

    def image_smoothing(self, image_path):
        """
        图像平滑处理
        :param image_path: 图片路径
        :return:
        """
        img = cv2.imread(image_path)
        img = cv2.resize(img, (0, 0), fx=0.5, fy=1)

        # 均值滤波
        # 简单的平均卷积操作
        blur = cv2.blur(img, (3, 3))

        # 方框滤波
        # 基本和均值一样，可以选择归一化
        box_1 = cv2.boxFilter(img, -1, (3, 3), normalize=True)
        # 容易越界
        box_2 = cv2.boxFilter(img, -1, (3, 3), normalize=True)

        # 高斯滤波
        # 高斯模糊的卷积核里的数值是满足高斯分布，相当于更重视中间的
        aussian = cv2.GaussianBlur(img, (5, 5), 1)

        # 中值滤波
        # 相当于用中值代替
        median = cv2.medianBlur(img, 5)  # 中值滤波

        # 展示所有的
        res = np.hstack((img, box_1, box_2, blur, aussian, median))
        self.image_show("image_smoothing", res)

    def image_erode(self, image_path):
        """
        图像腐蚀
        腐蚀操作。腐蚀是形态学图像处理中的一种基本操作，通常用于去除图像中的小噪声、分离物体或缩小物体的边界。
        cv2.erode(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]])
        src：输入图像，通常是二值图像（黑白图像）。
        kernel：用于腐蚀的结构元素（核）。可以通过 cv2.getStructuringElement 创建。
        iterations（可选）：腐蚀操作的次数，默认为 1。
        anchor（可选）：核的锚点位置，默认为核的中心。
        borderType（可选）：边界填充方式，默认为 cv2.BORDER_CONSTANT。
        borderValue（可选）：边界填充值，默认为 0。
        :param image_path: 图片路径
        :return:
        """
        img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
        kernel = np.ones((3, 3), np.uint8)
        erosion = cv2.erode(img, kernel, iterations=2)
        self.image_show("image_erode", erosion)

    def image_dilate(self, image_path):
        """
        图像膨胀
        膨胀操作。膨胀是形态学图像处理中的一种基本操作，通常用于扩大图像中的白色区域（前景）、填充小孔或连接断开的物体。
        cv2.erode(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]])
        src：输入图像，通常是二值图像（黑白图像）。
        kernel：用于腐蚀的结构元素（核）。可以通过 cv2.getStructuringElement 创建。
        iterations（可选）：腐蚀操作的次数，默认为 1。
        anchor（可选）：核的锚点位置，默认为核的中心。
        borderType（可选）：边界填充方式，默认为 cv2.BORDER_CONSTANT。
        borderValue（可选）：边界填充值，默认为 0。
        :param image_path: 图片路径
        :return:
        """
        img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
        kernel = np.ones((3, 3), np.uint8)
        erosion = cv2.dilate(img, kernel, iterations=2)
        self.image_show("image_dilate", erosion)

    def image_operations(self, image_path):
        """
        运算
        :param image_path: 图片路径
        :return:
        """
        img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
        kernel = np.ones((3, 3), np.uint8)

        # 开：先腐蚀，再膨胀  先腐蚀后膨胀，适合去除小噪声和分离物体
        opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
        self.image_show("image_operations_open", opening)

        # 闭：先膨胀，再腐蚀 先膨胀后腐蚀，适合填充小孔和连接断开的物体
        closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
        self.image_show("image_operations_close", closing)

        # 梯度=膨胀-腐蚀 用于提取图像的边缘或轮廓
        gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)
        self.image_show("image_operations_gradient", gradient)

        # 礼帽 = 原始输入-开运算结果 提取图像中比周围区域更亮的细小部分，通常用于背景不均匀的图像中提取微小目标或细节 显示毛刺
        tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)
        self.image_show("image_operations_tophat", tophat)

        # 黑帽 = 闭运算-原始输入  提取比周围更暗的细小部分，适用于暗目标检测  显示缺陷
        blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)
        self.image_show("image_operations_blackhat", blackhat)

    def image_gradient(self, image_path):
        """
        图像梯度
        dst = cv2.Sobel(src, ddepth, dx, dy, ksize)
        - ddepth:图像的深度
        - dx和dy分别表示水平和竖直方向
        - ksize是Sobel算子的大小
        :param image_path: 图片路径
        :return:
        """
        img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
        sobelx_1 = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
        # self.image_show("image_gradient_sobelx_1", sobelx_1)
        # 白到黑是正数，黑到白就是负数了，所有的负数会被截断成0，所以要取绝对值
        sobelx_1_ = cv2.convertScaleAbs(sobelx_1)
        # self.image_show("image_gradient_sobelx_1_", sobelx_1_)
        sobelx_2 = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
        # self.image_show("image_gradient_sobelx_2", sobelx_2)
        sobelx_2_ = cv2.convertScaleAbs(sobelx_2)
        # self.image_show("image_gradient_sobelx_2_", sobelx_2_)
        sobelxy = cv2.addWeighted(sobelx_1_, 0.5, sobelx_2_, 0.5, 0)
        self.image_show("image_gradient_sobelxy", sobelxy)

        scharrx = cv2.Scharr(img, cv2.CV_64F, 1, 0)
        scharry = cv2.Scharr(img, cv2.CV_64F, 0, 1)
        scharrx = cv2.convertScaleAbs(scharrx)
        scharry = cv2.convertScaleAbs(scharry)
        scharrxy = cv2.addWeighted(scharrx, 0.5, scharry, 0.5, 0)
        self.image_show("image_gradient_scharrxy", scharrxy)

        laplacian = cv2.Laplacian(img, cv2.CV_64F)
        laplacian = cv2.convertScaleAbs(laplacian)
        self.image_show("image_gradient_laplacian", laplacian)

    def image_edge_detection(self, image_path):
        """
        图像边缘检测

        Canny边缘检测
        - 1) 使用高斯滤波器，以平滑图像，滤除噪声。
        - 2) 计算图像中每个像素点的梯度强度和方向。
        - 3) 应用非极大值（Non-Maximum Suppression）抑制，以消除边缘检测带来的杂散响应。
        - 4) 应用双阈值（Double-Threshold）检测来确定真实的和潜在的边缘。
        - 5) 通过抑制孤立的弱边缘最终完成边缘检测。
        
        cv2.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]])
            image：输入图像，必须是单通道灰度图像。
            threshold1：低阈值，用于边缘连接。
            threshold2：高阈值，用于强边缘检测。
            edges（可选）：输出的边缘图像。
            apertureSize（可选）：Sobel 算子的孔径大小，默认为 3。
            L2gradient（可选）：是否使用 L2 范数计算梯度幅值。如果为 True，则使用更精确的 L2 范数；如果为 False，则使用 L1 范数（默认）。
        :param image_path: 图片路径
        :return:
        """

        img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
        v1 = cv2.Canny(img, 120, 250)
        v2 = cv2.Canny(img, 50, 100)
        res = np.hstack((v1, v2))
        self.image_show("image_edge_detection", res)

    def image_pyramid(self, image_path):
        """
        图像金字塔
        是图像处理中的一种多尺度表示方法，通过对图像进行多次降采样或上采样，生成一系列不同分辨率的图像
        :param image_path: 图片路径
        :return:
        """
        img = cv2.imread(image_path)

        # 高斯金字塔：向上采样方法（放大）
        up = cv2.pyrUp(img)
        self.image_show("image_pyramid_up", up)

        # 高斯金字塔：向下采样方法（缩小）
        down = cv2.pyrDown(img)
        self.image_show("image_pyramid_down", down)

        up_down = cv2.pyrDown(up)
        self.image_show("image_pyramid_up_down", up_down)

        img_up_down = img - up_down
        self.image_show("image_pyramid_img_up_down", img_up_down)

        down_up = cv2.resize(cv2.pyrUp(down), img.shape[:2])
        img_down_up = img - down_up

        res = np.hstack((img_up_down, img_down_up))
        self.image_show("image_pyramid_res", res)

    def image_contours(self, image_path):
        """
        图像轮廓
        contours, hierarchy = cv2.findContours(image, mode, method[, contours[, hierarchy[, offset]]])
        参数说明：
            image：输入的二值图像（黑白图像）。通常需要先对图像进行阈值处理或边缘检测。
            mode：轮廓检索模式，指定如何提取轮廓。常用模式包括：
                cv2.RETR_EXTERNAL：只检测最外层轮廓。
                cv2.RETR_LIST：检测所有轮廓，不建立层级关系。
                cv2.RETR_TREE：检测所有轮廓，并建立完整的层级关系。
            method：轮廓近似方法，指定如何存储轮廓点。常用方法包括：
                cv2.CHAIN_APPROX_NONE：存储所有轮廓点。
                cv2.CHAIN_APPROX_SIMPLE：压缩水平、垂直和对角线段，只存储端点。
            offset（可选）：轮廓点的偏移量。

        返回值：
            contours：检测到的轮廓列表。每个轮廓是一个点集（通常是 N x 1 x 2 的 NumPy 数组）。
            hierarchy：轮廓的层级信息，表示轮廓之间的关系
        :param image_path: 图片路径
        :return:
        """
        img = cv2.imread(image_path)
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

        contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)

        draw_img_1 = img.copy()
        """
        cv2.drawContours(image, contours, contourIdx, color[, thickness[, lineType[, hierarchy[, maxLevel[, offset]]]]])
            参数说明：
                image：输入图像，轮廓将绘制在这张图像上。
                contours：轮廓列表，通常由 cv2.findContours 返回。
                contourIdx：要绘制的轮廓索引。如果为负数（如 -1），则绘制所有轮廓。
                color：轮廓的颜色，格式为 (B, G, R)。
                thickness（可选）：轮廓线的粗细。如果为负数（如 -1），则填充轮廓内部。
                lineType（可选）：线的类型，默认为 cv2.LINE_8。
                hierarchy（可选）：轮廓的层级信息，通常由 cv2.findContours 返回。
                maxLevel（可选）：绘制轮廓的最大层级。默认为 0，表示只绘制指定的轮廓。
                offset（可选）：轮廓点的偏移量。"""
        res_1 = cv2.drawContours(draw_img_1, contours, -1, (0, 0, 255), 2)

        draw_img_2 = img.copy()
        res_2 = cv2.drawContours(draw_img_2, contours, 8, (0, 255, 255), 2)

        res = np.hstack((img, res_1, res_2))
        self.image_show("image_contours", res)

        epsilon = 0.08 * cv2.arcLength(contours[8], True)
        """
        approx = cv2.approxPolyDP(curve, epsilon, closed)
        参数说明：
            curve：输入的轮廓点集，通常是一个 N x 1 x 2 的 NumPy 数组。
            epsilon：近似精度参数，表示从原始轮廓到近似轮廓的最大距离。值越小，近似结果越接近原始轮廓。
            closed：布尔值，表示轮廓是否是封闭的。如果为 True，则假设轮廓是封闭的。
        返回值：
            approx：近似后的轮廓点集，通常是一个 M x 1 x 2 的 NumPy 数组，其中 M 是近似后的点数。"""
        contour = contours[8]
        approx = cv2.approxPolyDP(contour, epsilon, True)

        draw_img_3 = img.copy()
        res_3 = cv2.drawContours(draw_img_3, [approx], 0, (0, 0, 255), 2)

        """
        cv2.boundingRect(cnt)
            OpenCV库中的一个函数，用于计算轮廓的最小外接矩形（轴对齐矩形）。这个矩形是能够完全包围轮廓的最小矩形，其边与图像的 x 轴和 y 轴平行。
            x, y, w, h = cv2.boundingRect(cnt)
            x, y：矩形左上角的坐标。
            w, h：矩形的宽度和高度。
        """
        x, y, w, h = cv2.boundingRect(contour)
        img = cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
        """
        cv2.minEnclosingCircle(contour)
            OpenCV库中的一个函数，用于计算轮廓的最小外接圆。这个圆是能够完全包围轮廓的最小圆。
            (center_x, center_y), radius = cv2.minEnclosingCircle(contour)
            (center_x, center_y)：圆心坐标。
            radius：圆的半径。
        """
        (x, y), radius = cv2.minEnclosingCircle(contour)
        center = (int(x), int(y))
        radius = int(radius)
        img = cv2.circle(img, center, radius, (0, 255, 0), 2)
        res__2 = np.hstack((res_3, img))
        self.image_show("image_contours_res__2", res__2)

        print(f"轮廓维度 approx.shape: {approx.shape}")
        print(f"轮廓 approx: {approx}")
        print(f"type(contours): {type(contours)}")
        self.show_contours_info(contour)

    @staticmethod
    def show_contours_info(contour):
        """
        显示轮廓信息
        :param contour: 轮廓信息
        :return:
        """
        # 计算轮廓面积
        print(f"轮廓类型 type(contour): {type(contour)}")
        print(f"轮廓维度 contour.shape: {contour.shape}")
        area = cv2.contourArea(contour)
        print(f"轮廓面积 cv2.contourArea(contour): {area}")

        # 周长，True表示闭合的
        length = cv2.arcLength(contour, True)
        print(f"轮廓周长 cv2.arcLength(contour): {length}")

    @staticmethod
    def image_filtering(image_path):
        """
        图像滤波  傅里叶变换实现时域转为频域
            低通滤波器：只保留低频，会使得图像模糊
            高通滤波器：只保留高频，会使得图像细节增强

        opencv中主要就是cv2.dft()和cv2.idft()，输入图像需要先转换成np.float32 格式。
        - 得到的结果中频率为0的部分会在左上角，通常要转换到中心位置，可以通过shift变换来实现。
        - cv2.dft()返回的结果是双通道的（实部，虚部），通常还需要转换成图像格式才能展示（0,255）。
        :param image_path: 图片路径
        :return:
        """
        # 以灰度模式读取图像（单通道）
        img = cv2.imread(image_path, 0)
        img_float32 = np.float32(img)
        # 傅里叶变换
        """
        dft = cv2.dft(src[, dst[, flags[, nonzeroRows]]])
        参数说明：
            src：输入图像，必须是单通道（灰度图像）或双通道（实部和虚部）的浮点型矩阵。
            dst（可选）：输出结果，存储 DFT 的结果。
            flags（可选）：变换标志，常用的标志包括：
                cv2.DFT_COMPLEX_OUTPUT：输出复数结果（默认）。
                cv2.DFT_REAL_OUTPUT：输出实数结果（仅适用于逆 DFT）。
                cv2.DFT_SCALE：对结果进行缩放（通常用于逆 DFT）。
                cv2.DFT_INVERSE：执行逆 DFT。
            nonzeroRows（可选）：指定输入矩阵中非零行的数量，通常不需要设置。
        返回值：
            dft：DFT 的结果，通常是一个复数矩阵。"""

        dft = cv2.dft(img_float32, flags=cv2.DFT_COMPLEX_OUTPUT)
        # 对傅里叶变换的结果进行 频谱中心化。在图像处理中，傅里叶变换的结果通常会将低频分量放在图像的四个角落，而高频分量放在中心。
        # 通过 fftshift，可以将低频分量移到图像的中心，便于观察和分析。
        dft_shift = np.fft.fftshift(dft)
        # 得到灰度图能表示的形式
        # cv2.magnitude(dft_shift[:, :, 0], dft_shift[:, :, 1]) 是 OpenCV 中用于计算二维离散傅里叶变换（DFT）结果的幅度谱
        # 对数变换 np.log 将线性尺度转换为对数尺度，压缩高值区域的动态范围，增强低值区域的对比度，使高频细节更清晰可见
        magnitude_spectrum = 20 * np.log(cv2.magnitude(dft_shift[:, :, 0], dft_shift[:, :, 1]))

        from matplotlib import pyplot as plt
        plt.subplot(121), plt.imshow(img, cmap='gray')
        plt.title('Input Image'), plt.xticks([]), plt.yticks([])
        plt.subplot(122), plt.imshow(magnitude_spectrum, cmap='gray')
        plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
        plt.show()

        rows, cols = img.shape
        crow, ccol = int(rows / 2), int(cols / 2)  # 中心位置
        # 低通滤波
        mask_dwn = np.zeros((rows, cols, 2), np.uint8)
        mask_dwn[crow - 30:crow + 30, ccol - 30:ccol + 30] = 1

        fshift_d = dft_shift * mask_dwn
        # 还原中心位置
        f_ishift_d = np.fft.ifftshift(fshift_d)
        # 傅里叶逆变换
        img_back_d = cv2.idft(f_ishift_d)
        img_back_d = cv2.magnitude(img_back_d[:, :, 0], img_back_d[:, :, 1])

        # 高通滤波
        mask_up = np.ones((rows, cols, 2), np.uint8)
        mask_up[crow - 30:crow + 30, ccol - 30:ccol + 30] = 0
        fshift_up = dft_shift * mask_up
        # 还原中心位置
        f_ishift_up = np.fft.ifftshift(fshift_up)
        # 傅里叶逆变换
        img_back_up = cv2.idft(f_ishift_up)
        img_back_up = cv2.magnitude(img_back_up[:, :, 0], img_back_up[:, :, 1])

        plt.subplot(131), plt.imshow(img, cmap='gray')
        plt.title('Input Image'), plt.xticks([]), plt.yticks([])
        plt.subplot(132), plt.imshow(img_back_d, cmap='gray')
        plt.title('Result'), plt.xticks([]), plt.yticks([])
        plt.subplot(133), plt.imshow(img_back_up, cmap='gray')
        plt.title('Result'), plt.xticks([]), plt.yticks([])
        plt.show()

    def image_histogram(self, image_path):
        """
        图像直方图
        cv2.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]])
        参数说明
            images:
                输入图像列表。通常是一个包含单张图像的列表，例如 [image]。图像可以是灰度图或彩色图。
                原图像图像格式为 uint8 或 ﬂoat32。当传入函数时应 用中括号 [] 括来例如[img]
                类型: list 或 numpy.ndarray。
            channels:
                需要计算直方图的通道索引。对于灰度图像，通常是 [0]；对于彩色图像，可以是 [0]、[1]、[2] 分别对应 BGR 通道。
                类型: list。
            mask:
                掩码图像。如果提供，则只计算掩码区域内的像素直方图。如果不需要掩码，可以设为 None。
                类型: numpy.ndarray 或 None。
            histSize:
                直方图的 bin 数量。对于灰度图像，通常是 [256]，表示将像素强度分为 256 个 bin。
                类型: list。
            ranges:
                像素值的范围。对于 8 位图像，通常是 [0, 256]。
                类型: list。
            hist (可选):
                输出的直方图。如果提供，函数会将结果存储在这个数组中。
                类型: numpy.ndarray。
            accumulate (可选):
                是否累积直方图。如果为 True，则直方图不会被清零，而是在原有基础上累积。
                类型: bool，默认值为 False。
        返回值
            返回一个表示直方图的 numpy.ndarray 数组。
        :param image_path: 图片路径
        :return:
        """
        import matplotlib.pyplot as plt  # Matplotlib是RGB

        img_one = cv2.imread(image_path, 0)  # 0表示灰度图
        hist = cv2.calcHist([img_one], [0], None, [256], [0, 256])
        print(hist.shape)
        # img.ravel() 用于将多维数组展平为一维数组。对于图像处理任务，这个函数通常用于将图像的像素值展平为一个一维数组，方便进行统计分析或直方图计算
        plt.hist(img_one.ravel(), 256)
        plt.show()

        img_tre = cv2.imread(image_path)
        color = ('b', 'g', 'r')
        for i, col in enumerate(color):
            histr = cv2.calcHist([img_tre], [i], None, [256], [0, 256])
            plt.plot(histr, color=col)
            plt.xlim([0, 256])
        plt.show()

        mask = np.zeros(img_one.shape[:2], np.uint8)
        mask[100:300, 100:400] = 255
        masked_img = cv2.bitwise_and(img_one, img_one, mask=mask)  # 与操作
        mask_res = np.hstack((mask, masked_img))
        self.image_show("masked_img", mask_res)

        hist_full = cv2.calcHist([img_one], [0], None, [256], [0, 256])
        hist_mask = cv2.calcHist([img_one], [0], mask, [256], [0, 256])

        plt.plot(hist_full)
        plt.plot(hist_mask)
        plt.xlim([0, 256])
        plt.show()

        # cv2.equalizeHist  对图像进行直方图均衡化
        equ = cv2.equalizeHist(img_one)

        # 自适应直方图均衡化
        # 适用于局部对比度增强
        clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
        res_clahe = clahe.apply(img_one)
        res = np.hstack((img_one, equ, res_clahe))
        self.image_show("equalize", res)

        plt.close()

    def image_template_matching(self, image_path, template_path):
        """
        图像模板匹配
        cv2.matchTemplate 用于模板匹配的函数。模板匹配是一种在图像中查找与给定模板（小图像）最相似区域的技术。
        cv2.matchTemplate(image, templ, method[, result])
        参数说明
            image:
                输入图像。必须是 8 位或 32 位浮点型图像。
                类型: numpy.ndarray。
            templ:
                模板图像。大小不能超过输入图像，且类型必须与输入图像相同。
                类型: numpy.ndarray。
            method:
                匹配方法。OpenCV 提供了以下方法：
                cv2.TM_SQDIFF: 平方差匹配。计算平方不同，计算出来的值越小，越相关
                cv2.TM_SQDIFF_NORMED: 归一化平方差匹配。计算归一化平方不同，计算出来的值越接近0，越相关
                cv2.TM_CCORR: 相关性匹配。计算相关性，计算出来的值越大，越相关
                cv2.TM_CCORR_NORMED: 归一化相关性匹配。计算归一化相关性，计算出来的值越接近1，越相关
                cv2.TM_CCOEFF: 相关系数匹配。计算相关系数，计算出来的值越大，越相关
                cv2.TM_CCOEFF_NORMED: 归一化相关系数匹配。计算归一化相关系数，计算出来的值越接近1，越相关
                类型: int。
            result (可选):
                输出结果矩阵。如果提供，结果将存储在这个数组中。
                类型: numpy.ndarray。
        返回值
            返回一个结果矩阵，表示模板与输入图像每个区域的匹配程度。
            结果矩阵的大小为 (W - w + 1, H - h + 1)，其中 (W, H) 是输入图像的大小，(w, h) 是模板的大小。
        :param image_path: 图片路径
        :param template_path: 模板路径
        :return:
        """
        import matplotlib.pyplot as plt
        methods = ['cv2.TM_CCOEFF', 'cv2.TM_CCOEFF_NORMED', 'cv2.TM_CCORR',
                   'cv2.TM_CCORR_NORMED', 'cv2.TM_SQDIFF', 'cv2.TM_SQDIFF_NORMED']
        img = cv2.imread(image_path, 0)
        template = cv2.imread(template_path, 0)
        h, w = template.shape[:2]

        for meth in methods:
            img2 = img.copy()

            # 匹配方法的真值
            method = eval(meth)
            # 模板匹配
            res = cv2.matchTemplate(img, template, method)
            # 找到最匹配的位置
            min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)

            # 如果是平方差匹配TM_SQDIFF或归一化平方差匹配TM_SQDIFF_NORMED，取最小值
            if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
                top_left = min_loc
            else:
                top_left = max_loc
            bottom_right = (top_left[0] + w, top_left[1] + h)

            # 画矩形
            cv2.rectangle(img2, top_left, bottom_right, 255, 2)

            plt.subplot(121), plt.imshow(res, cmap='gray')
            plt.xticks([]), plt.yticks([])  # 隐藏坐标轴
            plt.subplot(122), plt.imshow(img2, cmap='gray')
            plt.xticks([]), plt.yticks([])
            plt.suptitle(meth)
            plt.show()

        img_rgb = cv2.imread(image_path)
        img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)
        template = cv2.imread(template_path, 0)
        h, w = template.shape[:2]

        res = cv2.matchTemplate(img_gray, template, cv2.TM_CCOEFF_NORMED)
        threshold = 0.8
        # 取匹配程度大于%80的坐标
        loc = np.where(res >= threshold)
        for pt in zip(*loc[::-1]):  # *号表示可选参数
            bottom_right = (pt[0] + w, pt[1] + h)
            cv2.rectangle(img_rgb, pt, bottom_right, (0, 0, 255), 2)

        self.image_show("img_gray", img_rgb)

    def image_features(self, image_path):
        """
        图像特征
        :param image_path: 图片路径
        :return:
        """
        img = cv2.imread(image_path)
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        """
        cv2.cornerHarris(src, blockSize, ksize, k[, dst[, borderType]])
        参数说明
            src:输入图像。必须是单通道 8 位或浮点型图像（灰度图）。
                类型: numpy.ndarray。
            blockSize:角点检测中使用的邻域大小。
                类型: int。
            ksize:Sobel 算子的孔径大小，用于计算图像的梯度。
                类型: int。
            k:Harris 角点检测方程中的自由参数，通常取值为 0.04 到 0.06。
                类型: float。
            dst (可选):输出图像。存储每个像素的角点响应值。
                类型: numpy.ndarray。
            borderType (可选):边界填充类型。默认值为 cv2.BORDER_DEFAULT。
                类型: int。
        返回值
            返回一个与输入图像大小相同的浮点型矩阵，表示每个像素的角点响应值。"""
        dst = cv2.cornerHarris(gray, 2, 3, 0.04)
        # 标记角点
        img[dst > 0.01 * dst.max()] = [0, 0, 255]
        self.image_show("image_features", img)

        # 需要 opencv-contrib-python
        # 得到特征点
        sift = cv2.SIFT_create()
        kp = sift.detect(gray, None)

        img_ = cv2.drawKeypoints(gray, kp, img)
        self.image_show("keypoints", img_)

    def image_features_match(self, image_a_path, image_b_path):
        """
        图像特征匹配
        :param image_a_path: 图片a路径
        :param image_b_path: 图片b路径
        :return:
        """
        image_a = cv2.imread(image_a_path, 0)
        image_b = cv2.imread(image_b_path, 0)
        sift = cv2.SIFT_create()

        kp1, des1 = sift.detectAndCompute(image_a, None)
        kp2, des2 = sift.detectAndCompute(image_b, None)

        # crossCheck表示两个特征点要互相匹，例如A中的第i个特征点与B中的第j个特征点最近的，并且B中的第j个特征点到A中的第i个特征点也是
        # NORM_L2: 归一化数组的(欧几里德距离)，如果其他特征计算方法需要考虑不同的匹配计算方式
        bf = cv2.BFMatcher(crossCheck=True)
        matches = bf.match(des1, des2)
        # 一对一匹配
        matches = sorted(matches, key=lambda x: x.distance)
        img3 = cv2.drawMatches(image_a, kp1, image_b, kp2, matches[:10], None, flags=2)

        self.image_show("image_features_match_img3", img3)

        # k对最佳匹配
        bf = cv2.BFMatcher()
        matches = bf.knnMatch(des1, des2, k=2)
        good = []
        for m, n in matches:
            if m.distance < 0.75 * n.distance:
                good.append([m])
        img4 = cv2.drawMatchesKnn(image_a, kp1, image_b, kp2, good, None, flags=2)
        self.image_show("image_features_match_img4", img4)

    def images_stitch(self, image_a_path, image_b_path):
        """
        图像拼接
        :param image_a_path: 图片a路径
        :param image_b_path: 图片b路径
        :return:
        """
        images_stitcher = ImagesStitcher()
        image_a = cv2.imread(image_a_path)
        image_b = cv2.imread(image_b_path)

        # 把图片拼接成全景图
        (result, vis) = images_stitcher.stitch([image_a, image_b], show_matches=True)

        # 显示所有图片
        self.image_show("Image A", image_a)
        self.image_show("Image B", image_b)
        self.image_show("Keypoint Matches", vis)
        self.image_show("Result", result)

    def face_detection(self, image_path):
        """
        人脸检测
        :param image_path: 图片路径
        :return:
        """
        self.image_info = cv2.imread(image_path)
        face_detector = cv2.CascadeClassifier("./xml/haarcascade_frontalface_alt.xml")
        image_change = cv2.cvtColor(self.image_info, cv2.COLOR_RGB2GRAY)
        coord = face_detector.detectMultiScale(image_change,
                                               scaleFactor=1.1,  # 缩放
                                               minNeighbors=3,  # 像素点
                                               minSize=(57, 57),  # 最小尺寸
                                               maxSize=(95, 95)  # 最大尺寸
                                               )
        for x, y, w, h in coord:
            cv2.rectangle(self.image_info, pt1=(x, y), pt2=(x + w, y + h), color=[0, 0, 255], thickness=2)
            cv2.circle(self.image_info, center=(x + w // 2, y + h // 2), radius=w // 2, color=[0, 255, 255],
                       thickness=2)
        self.image_show("face_detection")

    @staticmethod
    def cv2_image_add_text(img, text, left, top, text_color=(255, 0, 0), text_size=20):
        """
        CV2绘制中文
        """
        if isinstance(img, np.ndarray):  # 判断是否OpenCV图片类型
            img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))

        # 创建一个可以在给定图像上绘图的对象
        draw = ImageDraw.Draw(img)
        # 字体的格式
        font_style = ImageFont.truetype(
            "/usr/share/fonts/truetype/arphic/uming.ttc", text_size, encoding="utf-8")
        # 绘制文本
        draw.text((left, top), text, text_color, font=font_style)
        # 转换回OpenCV格式
        return cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR)


if __name__ == '__main__':
    image_man = OpencvManager()
    # image_man.read_image("images/cat.jpg")
    # image_man.read_video("images/test.mp4")
    # image_man.image_capture("images/cat.jpg")
    # image_man.color_channel_extraction("images/cat.jpg")
    # image_man.image_fusion("images/cat.jpg", "images/car.png")
    # image_man.image_size_change("images/cat.jpg")
    # image_man.image_grayscale("images/cat.jpg")
    # image_man.image_threshold("images/cat.jpg")
    # image_man.image_smoothing("images/cat.jpg")
    # image_man.image_erode("images/cat.jpg")
    # image_man.image_dilate("images/cat.jpg")
    # image_man.image_operations("images/cat.jpg")
    # image_man.image_gradient("images/pie.png")
    # image_man.image_gradient("images/car.png")
    # image_man.image_edge_detection("images/cat.jpg")
    # image_man.image_pyramid("images/lena.jpg")
    # image_man.image_contours('images/contours.png')
    # image_man.image_filtering('images/lena.jpg')
    # image_man.image_histogram('images/cat.jpg')
    # image_man.image_template_matching('images/mario.jpg', "images/mario_coin.jpg")
    # image_man.image_features('images/car.png')
    # image_man.image_features_match('images/left_01.png', "images/right_01.png")
    # image_man.images_stitch('images/left_01.png', "images/right_01.png")
